using System;
using System.IO;

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Cms;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities.IO;

namespace Org.BouncyCastle.Cms
{
	/**
	* General class for generating a CMS authenticated-data message stream.
	* <p>
	* A simple example of usage.
	* <pre>
	*      CMSAuthenticatedDataStreamGenerator edGen = new CMSAuthenticatedDataStreamGenerator();
	*
	*      edGen.addKeyTransRecipient(cert);
	*
	*      ByteArrayOutputStream  bOut = new ByteArrayOutputStream();
	*
	*      OutputStream out = edGen.open(
	*                              bOut, CMSAuthenticatedDataGenerator.AES128_CBC, "BC");*
	*      out.write(data);
	*
	*      out.close();
	* </pre>
	*/
	public class CmsAuthenticatedDataStreamGenerator
		: CmsAuthenticatedGenerator
	{
		private object              _originatorInfo = null;
		private object              _unprotectedAttributes = null;
		private int                 _bufferSize;
		private bool                _berEncodeRecipientSet;

		/**
		* base constructor
		*/
		public CmsAuthenticatedDataStreamGenerator()
		{
		}

		/**
		* constructor allowing specific source of randomness
		* @param rand instance of SecureRandom to use
		*/
		public CmsAuthenticatedDataStreamGenerator(
			SecureRandom rand)
			: base(rand)
		{
		}

		/**
		* Set the underlying string size for encapsulated data
		*
		* @param bufferSize length of octet strings to buffer the data.
		*/
		public void SetBufferSize(
			int bufferSize)
		{
			_bufferSize = bufferSize;
		}

		/**
		* Use a BER Set to store the recipient information
		*/
		public void SetBerEncodeRecipients(
			bool berEncodeRecipientSet)
		{
			_berEncodeRecipientSet = berEncodeRecipientSet;
		}

		/**
		* generate an enveloped object that contains an CMS Enveloped Data
		* object using the given provider and the passed in key generator.
		* @throws java.io.IOException
		*/
		private Stream Open(
			Stream				outStr,
			string				macOid,
			CipherKeyGenerator	keyGen)
		{
			SecretKey encKey = keyGen.generateKey();
			AlgorithmParameterSpec parameters = generateParameterSpec(macOID, encKey);

			ASN1EncodableVector recipientInfos = new ASN1EncodableVector();
			foreach (RecipientInf recipient in recipientInfs)
			{
				try
				{
					recipientInfos.Add(recipient.ToRecipientInfo(encKey, rand));
				}
				catch (IOException e)
				{
					throw new CmsException("encoding error.", e);
				}
				catch (InvalidKeyException e)
				{
					throw new CmsException("key inappropriate for algorithm.", e);
				}
				catch (GeneralSecurityException e)
				{
					throw new CmsException("error making encrypted content.", e);
				}
			}

			return Open(outStr, macOID, encKey, parameters, recipientInfos);
		}

		protected Stream Open(
			Stream        			outStr,
			string					macOid,
//			SecretKey				encKey,
//			AlgorithmParameterSpec	parameters,
			ICipherParameters		cipherParameters,
			Asn1EncodableVector		recipientInfos)
		{
			try
			{
				//
				// ContentInfo
				//
				BerSequenceGenerator cGen = new BerSequenceGenerator(outStr);

				cGen.AddObject(PkcsObjectIdentifiers.IdCTAuthData);

				//
				// Encrypted Data
				//
				BerSequenceGenerator envGen = new BerSequenceGenerator(cGen.GetRawOutputStream(), 0, true);

				envGen.AddObject(new DerInteger(AuthenticatedData.CalculateVersion(null)));

				if (_berEncodeRecipientSet)
				{
					envGen.GetRawOutputStream().Write(new BerSet(recipientInfos).GetEncoded());
				}
				else
				{
					envGen.GetRawOutputStream().Write(new DerSet(recipientInfos).GetEncoded());
				}

				Mac mac = CmsEnvelopedHelper.Instance.getMac(macOID, provider);

				mac.init(encKey, parameters);

				AlgorithmIdentifier macAlgId = GetAlgorithmIdentifier(macOID, parameters);

				envGen.GetRawOutputStream().Write(macAlgId.GetEncoded());

				BerSequenceGenerator eiGen = new BerSequenceGenerator(envGen.GetRawOutputStream());

				eiGen.AddObject(PkcsObjectIdentifiers.Data);

				BerOctetStringGenerator octGen = new BerOctetStringGenerator(eiGen.GetRawOutputStream(), 0, false);

				MacOutputStream mOut;

				if (_bufferSize != 0)
				{
					mOut = new MacOutputStream(octGen.GetOctetOutputStream(new byte[_bufferSize]), mac);
				}
				else
				{
					mOut = new MacOutputStream(octGen.GetOctetOutputStream(), mac);
				}

				return new CmsAuthenticatedDataOutputStream(mOut, cGen, envGen, eiGen);
			}
			catch (InvalidKeyException e)
			{
				throw new CmsException("key invalid in message.", e);
			}
			catch (NoSuchPaddingException e)
			{
				throw new CmsException("required padding not supported.", e);
			}
			catch (InvalidAlgorithmParameterException e)
			{
				throw new CmsException("algorithm parameter invalid.", e);
			}
			catch (InvalidParameterSpecException e)
			{
				throw new CmsException("algorithm parameter spec invalid.", e);
			}
			catch (IOException e)
			{
				throw new CmsException("exception decoding algorithm parameters.", e);
			}
		}

		/**
		* generate an enveloped object that contains an CMS Enveloped Data object
		*/
		public Stream Open(
			Stream	outStr,
			string	encryptionOid)
		{
			KeyGenerator keyGen = CmsEnvelopedHelper.Instance.createSymmetricKeyGenerator(encryptionOid);

			keyGen.Init(rand);

			return Open(outStr, encryptionOid, keyGen);
		}

		/**
		* generate an enveloped object that contains an CMS Enveloped Data object
		*/
		public Stream Open(
			Stream	outStr,
			string	encryptionOid,
			int		keySize)
		{
			KeyGenerator keyGen = CmsEnvelopedHelper.Instance.createSymmetricKeyGenerator(encryptionOID);

			keyGen.Init(keySize, rand);

			return Open(outStr, encryptionOID, keyGen);
		}

		private class CmsAuthenticatedDataOutputStream
			: BaseOutputStream
		{
			private readonly MacOutputStream		macStream;
			private readonly BerSequenceGenerator	cGen;
			private readonly BerSequenceGenerator	envGen;
			private readonly BerSequenceGenerator	eiGen;

			public CmsAuthenticatedDataOutputStream(
				MacOutputStream			macStream,
				BerSequenceGenerator	cGen,
				BerSequenceGenerator	envGen,
				BerSequenceGenerator	eiGen)
			{
				this.macStream = macStream;
				this.cGen = cGen;
				this.envGen = envGen;
				this.eiGen = eiGen;
			}

			public override void WriteByte(
				byte b)
			{
				macStream.WriteByte(b);
			}

			public override void Write(
				byte[]	bytes,
				int		off,
				int		len)
			{
				macStream.Write(bytes, off, len);
			}

			public override void Close()
			{
				macStream.Close();
				eiGen.Close();

				// [TODO] auth attributes go here           
				envGen.AddObject(new DerOctetString(macStream.GetMac()));
				// [TODO] unauth attributes go here

				envGen.Close();
				cGen.Close();
			}
		}
	}
}
